package org.cainiao.notebook.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import org.cainiao.api.lark.dto.response.docs.docs.apireference.document.LarkBlock;
import org.cainiao.api.lark.dto.response.docs.docs.apireference.document.LarkCallout;
import org.cainiao.api.lark.dto.response.docs.docs.apireference.document.LarkText;
import org.cainiao.api.lark.dto.response.docs.docs.apireference.document.text.LarkTextElement;
import org.cainiao.api.lark.dto.response.docs.docs.apireference.document.text.LarkTextStyle;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;

import java.io.Serial;
import java.io.Serializable;
import java.util.*;

/**
 * <br />
 * <p>
 * Author: Cai Niao(wdhlzd@163.com)<br />
 */
@Builder
@Data
public class CnBlock implements Serializable {

    @Serial
    private static final long serialVersionUID = 8034304016512704482L;

    /**
     * 块的唯一标识，为了兼容飞书等三方数据，用 String 类型代替 long
     */
    private String id;

    /**
     * 块类型<br />
     * <ol>
     *     <li>1：笔记，即一个笔记的根节点</li>
     *     <li>2：文本</li>
     *     <li>43：画板</li>
     * </ol>
     */
    private int type;

    /**
     * 根据不同的块类型，记录不同的值<br />
     * <ol>
     *     <li>笔记（1）：标题</li>
     *     <li>画板（19）：高亮块的图标</li>
     *     <li>画板（43）：引用画板资源的 token</li>
     * </ol>
     */
    @Schema(description = "根据不同的块类型，记录不同的值", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private String content;

    /**
     * 文本内容，因为文本的不同部分可能有不同的样式，因此将一个文本拆分为了元素列表
     */
    @Schema(description = "文本内容，因为文本的不同部分可能有不同的样式，因此将一个文本拆分为了元素列表", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private List<CnElement> elements;

    /**
     * 用不同的位表示不同的信息，4 字节，32 位<br />
     * <ol>
     *     <li>第 1 位：代办是否已办：0 代办；1 已办</li>
     *     <li>
     *         第 2 位：是否可折叠：0 不能折叠；1 可折叠
     *         说明：是否可折叠不能由块类型决定，因为希望多种类型都能选择是否可折叠
     *         从飞书块转换时，文本（有缩进的子节点的情况）、标题、有序或无序列表类型的块，有子节点时，置为 1
     *         从 notion 块转换时，是 notion 的可折叠块类型时置为 1
     *     </li>
     *     <li>第 3 位：是否折叠：0 展开；1 折叠</li>
     *     <li>第 4 位：代码块是否自动换行</li>
     *     <li>第 5 ~ 11 位：代码块的语言</li>
     *     <li>第 12 ~ 18 位：栅格的宽度比例</li>
     *     <li>第 19 ~ 23 位：背景颜色</li>
     *     <li>第 24 ~ 28 位：边线颜色</li>
     *     <li>第 29 ~ 30 位：align，对齐方式：0 靠左；1 居中；2 靠右</li>
     * </ol>
     */
    private int style;

    private static final byte STYLE_OFFSET_FOLD = 1;
    private static final byte STYLE_OFFSET_FOLDED = STYLE_OFFSET_FOLD + 1;
    private static final byte STYLE_OFFSET_WRAP = STYLE_OFFSET_FOLDED + 1;
    private static final byte STYLE_OFFSET_LANGUAGE = STYLE_OFFSET_WRAP + 1;
    private static final byte STYLE_OFFSET_WIDTH_RATIO = STYLE_OFFSET_LANGUAGE + 7;
    private static final byte STYLE_OFFSET_BACKGROUND_COLOR = STYLE_OFFSET_WIDTH_RATIO + 7;
    private static final byte STYLE_OFFSET_BORDER_COLOR = STYLE_OFFSET_BACKGROUND_COLOR + 5;
    // 32, 31, [30, 29], 28 ...
    private static final byte STYLE_OFFSET_ALIGN = STYLE_OFFSET_BORDER_COLOR + 5;

    /**
     * 掩码：代办是否完成
     */
    private static final int STYLE_MASK_DONE = 1;
    /**
     * 掩码：是否可折叠
     */
    private static final int STYLE_MASK_FOLD = 1 << STYLE_OFFSET_FOLD;
    /**
     * 掩码：是否折叠
     */
    private static final int STYLE_MASK_FOLDED = 1 << STYLE_OFFSET_FOLDED;
    /**
     * 掩码：代码块是否可换行
     */
    private static final int STYLE_MASK_WRAP = 1 << STYLE_OFFSET_WRAP;

    /**
     * 这个字段不用改为 ID 列表<br />
     * 因为一个块不会同时属于多个父块<br />
     * 因此在响应前端时，对 CnBlock 进行序列化时，将 children 改为 ID 列表并不能减少 I/O
     */
    @Schema(description = "子节点，如：文本缩进、标题折叠、无序/有序列表缩进", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
    private List<CnBlock> children;

    public static List<CnBlock> fromCnBlocks(List<LarkBlock> larkBlocks) {
        /*
         * 这里为了提高性能，假定飞书返回的块列表，父节点一定出现在子节点之前
         * 如果发现飞书返回数据并不满足这一要求再更改算法
         */
        List<CnBlock> cnBlocks = new ArrayList<>();
        // id 到 CnBlock 的映射
        Map<String, CnBlock> blockIdToLarkBlockMap = new HashMap<>();
        larkBlocks.forEach(larkBlock -> {
            String blockId = larkBlock.getBlockId();
            CnBlock currentCnBlock = fromLarkBlock(larkBlock);
            blockIdToLarkBlockMap.put(blockId, currentCnBlock);
            String parentId = larkBlock.getParentId();
            if (StringUtils.hasText(parentId)) {
                /*
                 * 只要 parentId 存在，就断言 blockIdToLarkBlockMap 中一定有数据，否则属于异常情况，要检查数据样本是否符合需求
                 * 这里为了性能省略 Assert 语句，避免频繁调用的开销
                 * 如果取不到值，下一句会报 NPE，起到了第一时间抛异常的作用，避免了问题延后暴露，等价于断言的效果
                 */
                CnBlock parentCnBlock = blockIdToLarkBlockMap.get(parentId);
                List<CnBlock> children_ = parentCnBlock.getChildren();
                if (children_ == null) {
                    children_ = new ArrayList<>();
                    parentCnBlock.setChildren(children_);
                }
                children_.add(currentCnBlock);
            } else {
                // 根节点
                cnBlocks.add(currentCnBlock);
            }
        });
        return cnBlocks;
    }

    private static void setContentFromFirstElement(CnBlock cnBlock, LarkText larkText) {
        cnBlock.setContent(larkText.getElements().get(0).getTextRun().getContent());
    }

    /**
     * 飞书块转换为菜鸟编辑器块，此处没有考虑 children<br />
     * // TODO 待完善
     *
     * @param larkBlock 飞书块
     * @return 菜鸟编辑器块
     */
    public static CnBlock fromLarkBlock(@NonNull LarkBlock larkBlock) {
        int type_ = larkBlock.getBlockType();
        CnBlock cnBlock = CnBlock.builder()
            .id(larkBlock.getBlockId())
            .type(type_)
            .build();
        LarkText larkText;
        LarkTextStyle larkTextStyle;
        switch (type_) {
            case 1 -> {
                // 根节点
                larkText = larkBlock.getPage();
                setContentFromFirstElement(cnBlock, larkText);
                initStyleAlign(cnBlock, larkText.getStyle());
            }
            case 2 -> initText(cnBlock, larkBlock.getText(), larkBlock);
            case 3 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading1());
            case 4 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading2());
            case 5 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading3());
            case 6 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading4());
            case 7 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading5());
            case 8 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading6());
            case 9 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading7());
            case 10 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading8());
            case 11 -> initHeading(cnBlock, larkBlock, larkBlock.getHeading9());
            case 12 -> initText(cnBlock, larkBlock.getBullet(), larkBlock);
            case 13 -> initText(cnBlock, larkBlock.getOrdered(), larkBlock);
            case 14 -> initCode(cnBlock, larkBlock);
            case 17 -> {
                // 代办事项
                larkText = larkBlock.getTodo();
                larkTextStyle = larkText.getStyle();
                if (larkTextStyle.isDone()) {
                    // 代办是否完成
                    cnBlock.setStyle(cnBlock.getStyle() | STYLE_MASK_DONE);
                }
                initStyleAlign(cnBlock, larkTextStyle);
            }
            case 19 -> {
                // 高亮块
                LarkCallout larkCallout = larkBlock.getCallout();
                cnBlock.setContent(larkCallout.getEmojiId());
                cnBlock.setStyle(cnBlock.getStyle()
                    | (larkCallout.getBackgroundColor() << STYLE_OFFSET_BACKGROUND_COLOR)
                    | (larkCallout.getBorderColor() << STYLE_OFFSET_BORDER_COLOR));
            }
            // 分栏列
            case 25 -> cnBlock.setStyle(cnBlock.getStyle()
                | (larkBlock.getGridColumn().getWidthRatio() << STYLE_OFFSET_WIDTH_RATIO));
            case 43 -> cnBlock.setContent(larkBlock.getBoard().getToken());
        }
        return cnBlock;
    }

    private static void initCode(CnBlock cnBlock, LarkBlock larkBlock) {
        LarkText larkText = larkBlock.getCode();
        cnBlock.setElements(getCnElementsFromLarkTextElements(larkText.getElements()));
        LarkTextStyle larkTextStyle = larkText.getStyle();
        int style_ = cnBlock.getStyle();
        if (larkTextStyle.isWrap()) {
            // 代码是否自动换行
            style_ |= STYLE_MASK_WRAP;
        }
        cnBlock.setStyle(style_ | (larkTextStyle.getLanguage() << STYLE_OFFSET_LANGUAGE));
    }

    private static void initText(CnBlock cnBlock, LarkText larkText, LarkBlock larkBlock) {
        LarkTextStyle larkTextStyle = larkText.getStyle();
        cnBlock.setElements(getCnElementsFromLarkTextElements(larkText.getElements()));
        initStyleAlign(cnBlock, larkTextStyle);
        initFold(cnBlock, larkBlock);
        initFolded(cnBlock, larkTextStyle);
    }

    private static void initHeading(CnBlock cnBlock, LarkBlock larkBlock, LarkText larkText) {
        setContentFromFirstElement(cnBlock, larkText);
        LarkTextStyle larkTextStyle = larkText.getStyle();
        initStyleAlign(cnBlock, larkTextStyle);
        initFold(cnBlock, larkBlock);
        initFolded(cnBlock, larkTextStyle);
    }

    /**
     * 初始化【是否可折叠】位<br />
     * 这里为初始化，因此认为 style 的 fold 位初始时为 0，因此只考虑置 1 的情况，不用考虑置 0
     *
     * @param cnBlock   菜鸟编辑器的块
     * @param larkBlock 飞书的块
     */
    private static void initFold(CnBlock cnBlock, LarkBlock larkBlock) {
        List<String> children_ = larkBlock.getChildren();
        if (children_ != null && !children_.isEmpty()) {
            cnBlock.setStyle(cnBlock.getStyle() | STYLE_MASK_FOLD);
        }
    }

    private static void initFolded(CnBlock cnBlock, @NonNull LarkTextStyle larkTextStyle) {
        if (larkTextStyle.isFolded()) {
            cnBlock.setStyle(cnBlock.getStyle() | STYLE_MASK_FOLDED);
        }
    }

    /**
     * 初始化【align 对齐】位<br />
     * 这里为初始化，因此认为 style 的 align 位初始时为 0，因此直接亦或即可，不用先截断
     *
     * @param cnBlock       菜鸟编辑器的块
     * @param larkTextStyle 飞书的 LarkTextStyle 样式对象
     */
    private static void initStyleAlign(CnBlock cnBlock, @NonNull LarkTextStyle larkTextStyle) {
        Integer larkAlign = larkTextStyle.getAlign();
        if (larkAlign == null) {
            return;
        }
        cnBlock.setStyle(cnBlock.getStyle() | ((larkAlign - 1) << STYLE_OFFSET_ALIGN));
    }

    public static List<CnElement> getCnElementsFromLarkTextElements(List<LarkTextElement> larkTextElements) {
        if (larkTextElements == null) {
            return Collections.emptyList();
        }
        return larkTextElements.stream().map(CnElement::fromLarkTextElement).toList();
    }
}
